<template>
  <el-form
    ref="FormRef"
    class="form-root"
    onsubmit="return false"
    :model="formData"
    v-bind="rootConfig"
  >
    <!-- 表单起始插槽 -->
    <slot name="prepend" />
    <!-- FormItem -->
    <el-row v-if="!rootConfig.inline" v-bind="rootConfig.row">
      <el-col
        v-for="item in formList"
        :key="item.prop"
        v-bind="item.itemConfig?.col || rootConfig.col"
      >
        <FormItemRender :item="item" />
      </el-col>
    </el-row>
    <template v-else>
      <template v-for="item in formList" :key="item.prop">
        <FormItemRender :item="item" />
      </template>
    </template>
    <!-- 表单末尾插槽 -->
    <slot name="append" />
  </el-form>
</template>

<script setup lang="jsx">
// slots
const labelSlot = item => <>{this.$slots.content?.({ name: item.labelSlot, item })}</>;
const componentSlot = item => <>{this.$slots.content?.({ name: item.slot, item })}</>;
const prepend = (item, config) => (
  <>{this.$slots.content?.({ name: config?.prependSlot || 'prepend', item, config })}</>
);
const append = (item, config) => (
  <>{this.$slots.content?.({ name: config?.appendSlot || 'append', item, config })}</>
);

// FormItem
const FormItemRender = ({ item }) => (
  <>
    <FormItem
      ref={el => setItemRef(el, item)}
      item={{ ...item, ...(tempConfig?.[item.prop] || {}) }}
      rootConfig={rootConfig}
      formData={formData}
      tempItemConfig={tempItemConfig?.[item.prop] || {}}
      tempBindConfig={tempBindConfig?.[item.prop] || {}}
      onFormEvent={formEvent}
      v-slots={{
        labelSlot: () => labelSlot(item),
        componentSlot: () => componentSlot(item),
        prepend: ({ config }) => prepend(item, config),
        append: ({ config }) => append(item, config),
      }}
    />
  </>
);
</script>

<script setup>
// 依赖
import _ from 'lodash';
// 组件
import FormItem from './item/FormItem.vue';

// Ref
const ItemRefs = {};
const FormRef = ref();

// 计算属性
const rootConfig = computed(() => {
  return {
    row: { gutter: 20 },
    col: { span: 24 },
    labelWidth: '100px',
    labelSuffix: '：',
    validateOnRuleChange: false,
    inline: false,
    ...formConfig,
  };
});

const tempConfig = ref({});
const tempBindConfig = ref({});
const tempItemConfig = ref({});

// 传入事件
const emit = defineEmits(['formEvent']);
// 传入属性
const { formConfig, formData, formList, ...props } = defineProps({
  /**
   * 表单配置
   */
  formConfig: {
    type: Object,
    default: () => ({}),
  },
  /**
   * 表单数据对象
   * 表单所有配置的属性需全部在该对象中赋值，
   * 否则重置时会出现表单选项无法输入问题
   */
  formData: {
    type: Object,
    default: () => ({}),
  },
  /**
   * 表单配置列表
   */
  formList: {
    type: Array,
    default: () => [],
  },
});

function formEvent(data) {
  /**
   * 表单项值改变
   *
   * @event formEvent
   * @param {string} prop 表单项改变的 属性名
   * @param {string} value 表单项改变的 属性值
   */
  emit('formEvent', { ...data, formData });
  let { linkInfo, eventName, prop, value } = data;
  if (linkInfo && eventName === 'change') {
    getLinkInfoData(linkInfo);
  }
}

function getLinkInfoData(linkInfo, isInit = false) {
  /**
   *  联动请求
   */
  linkInfo.linkProp.forEach(item => {
    ItemRefs[item]?.ComponentRef?.getDataList(isInit);
  });
}

function setItemRef(el, item) {
  if (el) {
    ItemRefs[item?.prop] = el;
  }
}

// 暴露函数
function setConfig(prop, configProp, value) {
  /**
   * 设置表单某个配置项 属性值， 常用动态更改是否显示
   * @param {string} prop 配置项 prop 值
   * @param {string} configProp 要修改配置项中的属性名
   * @param {any} value 要修改配置项中的属性值
   */
  const propFormItem = formList.find(item => item.prop === prop);
  if (propFormItem) {
    tempConfig.value[prop] = { [configProp]: value };
  }
}

function setBindConfig(prop, bindProp, value) {
  /**
   * 设置表单某个配置项的 bindConfig 属性值， 常用动态更改下拉框选项
   * @param {string} prop 配置项 prop 值
   * @param {string} bindProp 要修改配置项中 bindConfig 的属性名
   * @param {any} value 要修改配置项中 bindConfig 的属性值
   * @public
   */
  const propFormItem = formList.find(item => item.prop === prop);
  const bindConfig = propFormItem && propFormItem.bindConfig;
  if (bindConfig) {
    tempBindConfig.value[prop] = { [bindProp]: value };
  }
}

function setItemConfig(prop, itemProp, value) {
  /**
   * 设置表单某个配置项的 bindConfig 属性值， 常用动态更改下拉框选项
   * @param {string} prop 配置项 prop 值
   * @param {string} itemProp 要修改配置项中 bindConfig 的属性名
   * @param {any} value 要修改配置项中 bindConfig 的属性值
   * @public
   */
  const propFormItem = formList.find(item => item.prop === prop);
  const itemConfig = propFormItem && propFormItem.itemConfig;
  if (itemConfig) {
    tempItemConfig.value[prop] = { [itemProp]: value };
  }
}

function resetFields() {
  /**
   * 重置表单数据， 回到默认值
   */
  FormRef.value.resetFields();
}

function clearValidate(prop) {
  /**
   * 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组，如不传则移除整个表单的校验结果
   */
  FormRef.value.clearValidate(prop);
}

function getFormItemRef(prop) {
  if (_.isArray(prop)) {
    return prop.map(i => ItemRefs[i]);
  } else {
    return ItemRefs[prop];
  }
}

function getFileList(prop, list) {
  /**
   * 附件内容回填
   */
  nextTick(() => {
    ItemRefs[prop]?.ComponentRef.value?.init(list);
  });
}

function editorSetText(prop, content) {
  /**
   * 富文本编辑器内容重置
   */
  nextTick(() => {
    ItemRefs[prop]?.ComponentRef.value?.setHtmlText(content);
  });
}

function validate(callback, onError) {
  /**
   * 表单校验
   * @param {function} callback 校验通过后回调函数
   * @param {function} onError 校验失败后回调函数
   */
  FormRef.value.validate(valid => {
    if (valid) {
      callback();
    } else {
      if (onError) onError();
      return false;
    }
  });
}

defineExpose({
  setConfig,
  setBindConfig,
  setItemConfig,
  validate,
  resetFields,
  clearValidate,
  getFormItemRef,
  getFileList,
  editorSetText,
  getLinkInfoData,
});
</script>

<style scoped lang="scss">
.form-root :deep() {
  .el-form-item {
    margin-bottom: 12px;
    .el-form-item__content {
      width: 100%;
      .el-cascader,
      .el-input,
      .el-select {
        width: 100%;
      }
      .el-input__wrapper {
        width: 100%;
        box-sizing: border-box;
      }
    }
  }
  .el-form-item__label {
    padding: 0;
  }
  .el-form--label-top {
    line-height: 14px;
  }
}
.el-form--inline :deep() {
  .el-form-item {
    margin-right: unset;
  }
}
</style>
